﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;

namespace Cfg.Proxies
{
    public class ProxyBuilder
    {
        const MethodAttributes GetterSetterMethodAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual;
        const string AssemblyName = "cfg.proxies";

        private static readonly Dictionary<Type, Type> ProxyTypes = new Dictionary<Type, Type>();

        private static ModuleBuilder _moduleBuilder;
        private static ModuleBuilder ModuleBuilder
        {
            get
            {
                if (_moduleBuilder != null)
                    return _moduleBuilder;

                var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(AssemblyName), AssemblyBuilderAccess.RunAndSave);
                _moduleBuilder = ab.DefineDynamicModule(AssemblyName, AssemblyName + ".dll");
                return _moduleBuilder;
            }
        }

        private static PropertyInfo[] GetPublicProperties(Type type)
        {
            if (type.IsInterface)
            {
                var propertyInfos = new List<PropertyInfo>();

                var considered = new List<Type>();
                var queue = new Queue<Type>();
                considered.Add(type);
                queue.Enqueue(type);
                while (queue.Count > 0)
                {
                    var subType = queue.Dequeue();
                    foreach (var subInterface in subType.GetInterfaces())
                    {
                        if (considered.Contains(subInterface)) continue;

                        considered.Add(subInterface);
                        queue.Enqueue(subInterface);
                    }

                    var typeProperties = subType.GetProperties(
                        BindingFlags.FlattenHierarchy
                        | BindingFlags.Public
                        | BindingFlags.Instance);

                    var newPropertyInfos = typeProperties
                        .Where(x => !propertyInfos.Contains(x));

                    propertyInfos.InsertRange(0, newPropertyInfos);
                }

                return propertyInfos.ToArray();
            }

            return type.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);
        }

        /// <summary>
        /// Creates proxy type which overrides only properties
        /// </summary>
        /// <param name="baseType"></param>
        /// <returns></returns>
        private static Type GetProxyType(Type baseType)
        {
            if (ProxyTypes.ContainsKey(baseType))
                return ProxyTypes[baseType];

            var proxyTypeName = string.Format("{0}_Proxy_{1:N}", baseType.Name, Guid.NewGuid());

            // proxy is public class inherited from ProxyBase
            TypeBuilder tb = ModuleBuilder.DefineType(proxyTypeName, TypeAttributes.Public, typeof(ProxyBase));

            // proxy implements type T
            tb.AddInterfaceImplementation(baseType);

            //var propertiesNeedingProxy = new List<string>();

            foreach (var propInfo in GetPublicProperties(baseType))
            {
                var propName = propInfo.Name;
                //var key = prefix == null ? propName : string.Format("{0}.{1}", prefix, propName);
                var propType = propInfo.PropertyType;
              
                /*
                if (propType.IsInterface)
                {
                    propertiesNeedingProxy.Add(propName);
                    //GetProxyType(propType, key);
                }
                */

                //create the backing field
                FieldBuilder backingField = tb.DefineField("_" + propName, propType, FieldAttributes.Private);

                //create property
                PropertyBuilder prop = tb.DefineProperty(propName, PropertyAttributes.SpecialName, propType, null);

                //create property Getter
                MethodBuilder propGetter = tb.DefineMethod("get_" + propName, GetterSetterMethodAttributes, propType, Type.EmptyTypes);
                ILGenerator ilPropGetter = propGetter.GetILGenerator();

                ilPropGetter.Emit(OpCodes.Ldarg_0);
                ilPropGetter.Emit(OpCodes.Ldarg_0);

                ilPropGetter.Emit(OpCodes.Ldfld, backingField);
                ilPropGetter.Emit(OpCodes.Ldstr, propName);
                ilPropGetter.Emit(OpCodes.Call,
                    typeof(ProxyBase).GetMethod("GetValue", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
                        .MakeGenericMethod(propType));

                ilPropGetter.Emit(OpCodes.Ret);

                // create property Setter
                MethodBuilder propSetter = tb.DefineMethod("set_" + propName, GetterSetterMethodAttributes, typeof(void), new[] { propType });
                ILGenerator ilPropSetter = propSetter.GetILGenerator();
                ilPropSetter.Emit(OpCodes.Ldarg_0);
                ilPropSetter.Emit(OpCodes.Ldarg_1);
                ilPropSetter.Emit(OpCodes.Stfld, backingField);
                ilPropSetter.Emit(OpCodes.Ret);

                //assign getter and setter
                prop.SetGetMethod(propGetter);
                prop.SetSetMethod(propSetter);
            }

            // default constructor
            tb.DefineDefaultConstructor(MethodAttributes.Public);
            
            // Finish the type.
            var proxyType = tb.CreateType();

            ProxyTypes[baseType] = proxyType;
            return proxyType;
        }

        /// <summary>
        /// Creates proxy instance
        /// </summary>
        /// <param name="baseType"></param>
        /// <param name="internalNamespace"></param>
        /// <returns></returns>
        public static object CreateProxy(Type baseType, string internalNamespace = null)
        {
            var proxyType = GetProxyType(baseType);
            var proxy = Activator.CreateInstance(proxyType) as ProxyBase;
            proxy.InternalNamespace = internalNamespace;

            foreach (var propInfo in proxyType.GetProperties().Where(x => x.PropertyType.IsInterface))
            {
                var @namespace = internalNamespace == null ? propInfo.Name : string.Format("{0}.{1}", internalNamespace, propInfo.Name);

                var propertyProxy = CreateProxy(propInfo.PropertyType, @namespace);
                propInfo.SetValue(proxy, propertyProxy, null);
            }
            
            return proxy;
        }

        /// <summary>
        /// Creates proxt instance
        /// </summary>
        /// <typeparam name="T">base type</typeparam>
        /// <returns></returns>
        public static T CreateProxy<T>()
        {
            var baseType = typeof (T);
            return (T)CreateProxy(baseType);
        }
    }
}
